<script setup lang="ts">
import { Icon } from '@/components/icon'
import { NativeScroll } from '@/components/native-scroll'
import { Overflow } from '@/components/overflow'
import { Popper } from '@/components/popper'
import { Renderer } from '@/components/renderer'
import { Tag } from '@/components/tag'
import { Tooltip } from '@/components/tooltip'
import { useFieldStore } from '@/components/form'

import { computed, nextTick, onBeforeUpdate, reactive, ref, toRef, watch, watchEffect } from 'vue'

import CascaderPanel from './cascader-panel.vue'
import {
  createIconProp,
  createSizeProp,
  createStateProp,
  emitEvent,
  useIcons,
  useLocale,
  useNameHelper,
  useProps,
} from '@vexip-ui/config'
import { placementWhileList, useClickOutside, useHover, usePopper } from '@vexip-ui/hooks'
import { flatTree, getLast, isNull, isPromise, toAttrValue, transformTree } from '@vexip-ui/utils'
import { cascaderProps } from './props'

import type { PopperExposed } from '@/components/popper'
import type {
  CascaderKeyConfig,
  CascaderOptionState,
  CascaderSlots,
  CascaderValue,
  Data,
} from './symbol'

type ChangeListener = (value: CascaderValue, data: Data[] | Data[][]) => void

const ID_KEY = Symbol('ID_KEY')
const PARENT_KEY = Symbol('PARENT_KEY')

const defaultKeyConfig: Required<CascaderKeyConfig> = {
  value: 'value',
  label: 'label',
  children: 'children',
  disabled: 'disabled',
  hasChild: 'hasChild',
}

defineOptions({ name: 'Cascader' })

const {
  idFor,
  labelId,
  state,
  disabled,
  loading,
  size,
  validateField,
  clearField,
  getFieldValue,
  setFieldValue,
} = useFieldStore<CascaderValue>(() => reference.value?.focus())

const _props = defineProps(cascaderProps)
const nh = useNameHelper('cascader')
const props = useProps('cascader', _props, {
  size: createSizeProp(size),
  state: createStateProp(state),
  locale: null,
  value: {
    default: () => getFieldValue(),
    static: true,
  },
  visible: {
    default: false,
    static: true,
  },
  options: {
    default: () => [],
    static: true,
  },
  placeholder: null,
  prefix: createIconProp(),
  prefixColor: '',
  suffix: createIconProp(),
  suffixColor: '',
  noCascaded: false,
  multiple: false,
  disabled: () => disabled.value,
  clearable: false,
  placement: {
    default: 'bottom-start',
    validator: value => placementWhileList.includes(value),
  },
  transfer: false,
  staticSuffix: false,
  noSuffix: false,
  transitionName: () => nh.ns('drop'),
  outsideClose: true,
  keyConfig: () => ({}),
  separator: {
    default: '/',
    validator: value => value.length === 1,
  },
  hoverTrigger: false,
  maxTagCount: 0,
  briefLabel: false,
  noRestTip: false,
  onAsyncLoad: {
    default: null,
    isFunc: true,
  },
  mergeTags: false,
  tagType: null,
  emptyText: null,
  loading: () => loading.value,
  loadingIcon: createIconProp(),
  loadingLock: false,
  loadingEffect: null,
  transparent: false,
  popperAlive: null,
  slots: () => ({}),
})

const emit = defineEmits(['update:visible', 'update:value'])

const slots = defineSlots<CascaderSlots>()

const icons = useIcons()
const locale = useLocale('select', toRef(props, 'locale'))

const currentVisible = ref(props.visible)
const currentValues = ref<string[]>([])
const currentLabels = ref<string[]>([])
const mergedValues = ref<string[]>([])
const mergedLabels = ref<string[]>([])
const isPopperShow = ref(false)
const placement = toRef(props, 'placement')
const transfer = toRef(props, 'transfer')

const emittedValue = ref<CascaderValue | null>(null)
const optionTree = ref<CascaderOptionState[]>(null!)
const isAsyncLoad = computed(() => typeof props.onAsyncLoad === 'function')
const usingMerged = computed(() => props.mergeTags && !props.noCascaded)
const templateValues = computed(() =>
  usingMerged.value ? mergedValues.value : currentValues.value,
)
const templateLabels = computed(() =>
  usingMerged.value ? mergedLabels.value : currentLabels.value,
)

let optionList: CascaderOptionState[] = null!
let optionIdMap: Map<number, CascaderOptionState> = null!
let optionValueMap: Map<string, CascaderOptionState> = null!
let outsideClosed = false
let prevClosedId = -1
let flattedOptions: Record<any, any>[]

const updateTrigger = ref(0)

watchEffect(() => {
  /* eslint-disable @typescript-eslint/no-unused-expressions */
  props.keyConfig.value
  props.keyConfig.label
  props.keyConfig.disabled
  props.keyConfig.hasChild
  props.separator
  isAsyncLoad.value
  /* eslint-enable */

  flattedOptions = flatTree(props.options as Record<any, any>[], {
    keyField: ID_KEY,
    parentField: PARENT_KEY,
    childField: props.keyConfig.children ?? defaultKeyConfig.children,
    forceInject: true,
  })

  updateTrigger.value++
})

watch(updateTrigger, initOptionStates, { immediate: true })

function initOptionStates() {
  const separator = props.separator
  const isAsync = isAsyncLoad.value

  optionList = createOptionStates(flattedOptions)
  optionIdMap = new Map()
  optionValueMap = new Map()

  for (let i = 0, len = optionList.length; i < len; ++i) {
    const option = optionList[i]

    initOptionFull(option, separator)
    optionIdMap.set(option.id, option)
    optionValueMap.set(option.fullValue, option)

    if (isAsync) {
      option.childrenLoaded = queryChildrenLoaded(option)
    }
  }

  optionTree.value = transformTree(optionList)
  initValueAndLabel(emittedValue.value)
}

const openedIds = ref<number[]>([])
const optionsList = computed(() => {
  return [
    optionTree.value,
    ...openedIds.value.map(id => optionIdMap.get(id)?.children).filter(Boolean),
  ]
})

const wrapper = useClickOutside(handleClickOutside)
const popper = ref<PopperExposed>()
const { reference, transferTo, updatePopper } = usePopper({
  placement,
  transfer,
  wrapper,
  popper: computed(() => popper.value?.wrapper),
  isDrop: true,
})
const { isHover } = useHover(reference)

const panelElList = ref<InstanceType<typeof CascaderPanel>[]>([])
const restTagCount = ref(0)
const restTipShow = ref(false)
const selectorWidth = ref(0)

const className = computed(() => {
  return {
    [nh.b()]: true,
    [nh.ns('input-vars')]: true,
    [nh.bs('vars')]: true,
    [nh.bm('inherit')]: props.inherit,
    [nh.bm('multiple')]: props.multiple,
    [nh.bm('responsive')]: props.multiple && props.maxTagCount <= 0,
  }
})
const readonly = computed(() => props.loading && props.loadingLock)
const selectorClass = computed(() => {
  const baseCls = nh.be('selector')

  return {
    [baseCls]: true,
    [`${baseCls}--focused`]: !props.disabled && currentVisible.value,
    [`${baseCls}--disabled`]: props.disabled,
    [`${baseCls}--readonly`]: readonly.value,
    [`${baseCls}--loading`]: props.loading,
    [`${baseCls}--${props.size}`]: props.size !== 'default',
    [`${baseCls}--${props.state}`]: props.state !== 'default',
    [`${baseCls}--has-prefix`]: hasPrefix.value,
    [`${baseCls}--has-suffix`]: !props.noSuffix,
    [`${baseCls}--transparent`]: props.transparent,
  }
})
const hasPrefix = computed(() => {
  return !!(slots.prefix || props.slots.prefix || props.prefix)
})
const hasValue = computed(() => !!templateValues.value[0])
const usingHover = computed(() => props.hoverTrigger && !isAsyncLoad.value)
const showClear = computed(() => {
  return !props.disabled && !readonly.value && props.clearable && isHover.value && hasValue.value
})

watch(
  () => props.visible,
  value => {
    currentVisible.value = value
  },
)
watch(currentVisible, async value => {
  if (value) {
    restTipShow.value = false
    selectorWidth.value = wrapper.value?.offsetWidth || 0

    await updatePopper()
    nextTick(() => {
      getLast(panelElList.value)?.$el?.focus()
    })
  } else {
    isPopperShow.value = false

    if (reference.value && !outsideClosed) {
      reference.value.focus()
    }
  }

  outsideClosed = false
})

let outsideChanged = false

watch(emittedValue, () => {
  outsideChanged = true
})
watch(
  () => props.value,
  value => {
    if (value !== emittedValue.value || outsideChanged) {
      emittedValue.value = value
      initValueAndLabel(value)
      nextTick(() => {
        outsideChanged = false
      })
    }
  },
  { immediate: true },
)
watch(
  () => props.briefLabel,
  brief => {
    currentLabels.value = currentValues.value
      .map(value => optionValueMap.get(value)?.[brief ? 'label' : 'fullLabel'] as string)
      .filter(Boolean)

    // nextTick(computeTagsOverflow)
  },
)
watch(isAsyncLoad, value => {
  if (value) {
    for (const option of optionIdMap.values()) {
      option.childrenLoaded = queryChildrenLoaded(option)
    }
  }
})
watch(usingMerged, value => {
  if (value) {
    mergedValues.value.length = 0
    mergedLabels.value.length = 0

    updateMergedProps()
  }

  if (isAsyncLoad.value) {
    const originalOptions: CascaderOptionState[] = []

    for (const option of optionIdMap.values()) {
      if (option.checked) {
        originalOptions.push(option)
      }

      if (option.hasChild && !option.children.length && !option.loaded) {
        option.checked = false
      }
    }

    for (let i = 0, len = originalOptions.length; i < len; ++i) {
      const option = originalOptions[i]

      updateCheckedUpward(option)
      updateCheckedDown(option)
    }
  }

  emitMultipleChange()
})
watch(
  () => optionsList.value.length,
  () => {
    updatePopper()
    nextTick(() => {
      getLast(panelElList.value)?.$el?.focus()
      prevClosedId = -1
    })
  },
)
watch(
  () => props.disabled,
  value => {
    if (value) {
      setVisible(false)
    }
  },
)
watch(readonly, value => {
  if (value) {
    setVisible(false)
  }
})

onBeforeUpdate(() => {
  panelElList.value.length = 0
})

defineExpose({
  idFor,
  labelId,
  currentVisible,
  currentValues,
  currentLabels,
  isHover,
  openedIds,
  restTagCount,
  restTipShow,
  optionsList,
  hasValue,
  templateValues,
  templateLabels,
  showClear,
  wrapper,
  reference,
  popper,
  panelElList,
  handlePanelOpen,
  handleOptionSelect,
  handleOptionCheck,
  toggleVisible,
  handleClear,
  handlePanelBack,
  focus: (options?: FocusOptions) => reference.value?.focus(options),
  blur: () => reference.value?.blur(),
})

function createOptionStates(rawOptions: Record<string | symbol, any>[]) {
  const {
    value: valueKey,
    label: labelKey,
    disabled: disabledKey,
    hasChild: hasChildKey,
  } = { ...defaultKeyConfig, ...props.keyConfig }

  return rawOptions.map(rawOption => {
    const {
      [ID_KEY]: id,
      [PARENT_KEY]: parent,
      [valueKey]: value,
      [labelKey]: label,
      [disabledKey]: disabled,
      [hasChildKey]: hasChild,
    } = rawOption

    return reactive<CascaderOptionState>({
      id,
      parent,
      value,
      disabled,
      hasChild,
      label: label || String(value),
      fullValue: '',
      fullLabel: '',
      children: [],
      checked: false,
      partial: false,
      loading: false,
      loaded: false,
      error: false,
      childrenLoaded: false,
      data: rawOption,
    })
  })
}

function initOptionFull(option: CascaderOptionState, separator: string) {
  let value = option.value as string
  let label = option.label
  let parent = optionIdMap.get(option.parent)

  while (parent) {
    value = `${parent.value}${separator}${value}`
    label = `${parent.label}${separator}${label}`
    parent = optionIdMap.get(parent.parent)
  }

  option.fullValue = value
  option.fullLabel = label
}

function queryChildrenLoaded(option: CascaderOptionState) {
  if (option.hasChild && !option.children?.length) {
    return option.loaded
  }

  const loop: CascaderOptionState[] = [...option.children]

  while (loop.length) {
    const child = loop.shift()!

    if (child.childrenLoaded) continue

    if (child.hasChild && !child.children?.length) {
      child.childrenLoaded = child.loaded

      if (!child.loaded) return false
    }

    loop.push(...child.children)
  }

  return true
}

function updateMergedProps() {
  const baseValues = isAsyncLoad.value
    ? currentValues.value.concat(mergedValues.value)
    : currentValues.value
  const values = new Set(baseValues)
  const loop = [...baseValues]

  while (loop.length) {
    const value = loop.shift()!

    const option = optionValueMap.get(value)

    if (option) {
      const parent = optionIdMap.get(option.parent)

      if (parent?.checked) {
        values.delete(value)
        values.add(parent.fullValue)
        loop.push(parent.fullValue)
      }
    }
  }

  const briefLabel = props.briefLabel

  mergedValues.value = Array.from(values).filter(value => optionValueMap.has(value))
  mergedLabels.value = mergedValues.value
    .map(value => {
      const option = optionValueMap.get(value)!

      return briefLabel ? option.label : option.fullLabel
    })
    .filter(Boolean)
}

function isFlatArray<T extends string | number>(value: T[] | T[][]): value is T[] {
  return !!value.length && !Array.isArray(value[0])
}

function isComplexArray<T extends string | number>(value: T[] | T[][]): value is T[][] {
  return !!value.length && Array.isArray(value[0])
}

function initValueAndLabel(value: CascaderValue | null) {
  const processMerged = () => {
    if (usingMerged.value) {
      if (isAsyncLoad.value) {
        mergedValues.value = Array.from(optionIdMap.values())
          .filter(option => option.checked)
          .map(option => option.fullValue)
      }

      updateMergedProps()
    }
  }

  for (const option of optionList) {
    option.checked = false
    option.partial = false
  }

  if (!value?.length) {
    currentValues.value = []
    currentLabels.value = []
    processMerged()
    return
  }

  const briefLabel = props.briefLabel

  if (props.multiple) {
    const normalizedValue = isFlatArray(value) ? [value] : value
    const valueSet = new Set<string>(normalizedValue.map(v => v.join(props.separator)))
    const selectedValues: string[] = []
    const selectedLabels: string[] = []
    const selectedOptions: CascaderOptionState[] = []

    valueSet.forEach(value => {
      const option = optionValueMap.get(value)

      if (option) {
        option.checked = true
        option.partial = false

        selectedValues.push(value)
        selectedLabels.push(briefLabel ? option.label : option.fullLabel)
        selectedOptions.push(option)
      }
    })

    if (!props.noCascaded) {
      const originalOptions = selectedOptions.concat(
        Array.from(optionIdMap.values()).filter(option => option.disabled && option.checked),
      )

      for (let i = 0, len = originalOptions.length; i < len; ++i) {
        const option = originalOptions[i]

        updateCheckedUpward(option)
        updateCheckedDown(option)
      }
    }

    currentValues.value = selectedValues
    currentLabels.value = selectedLabels
  } else {
    const normalizedValue = isComplexArray(value) ? value[0] : value
    const stringValue = normalizedValue.join(props.separator)
    const option = optionValueMap.get(stringValue)

    if (option) {
      currentValues.value = [stringValue]
      currentLabels.value = [briefLabel ? option.label : option.fullLabel]

      if (props.noCascaded) {
        option.checked = true
        option.partial = false
      }
    } else {
      currentValues.value = []
      currentLabels.value = []
    }
  }

  processMerged()

  if (openedIds.value.length) return

  const firstValue = currentValues.value[0]

  if (firstValue) {
    const option = optionValueMap.get(firstValue)!
    const ids = [option.id]

    let parent = optionIdMap.get(option.parent)

    while (parent) {
      ids.push(parent.id)
      parent = optionIdMap.get(parent.parent)
    }

    openedIds.value = ids.reverse().slice(0, -1)
  }
}

function setVisible(visible: boolean) {
  if (currentVisible.value === visible) return

  currentVisible.value = visible

  emit('update:visible', visible)
  emitEvent(props.onToggle, visible)
}

async function handlePanelOpen(option: CascaderOptionState, depth: number) {
  if (!option.hasChild && !option.children?.length) return

  if (isAsyncLoad.value && !option.children?.length && !option.loaded) {
    option.loading = true

    let result: ReturnType<typeof props.onAsyncLoad>

    try {
      result = props.onAsyncLoad(option.data)
      result = isPromise(result) ? await result : result
    } catch (e) {
      option.error = true
      option.loading = false
      return
    }

    const rawOptions = result as any[]

    if (!Array.isArray(rawOptions) || !rawOptions.length) {
      option.hasChild = false
    } else {
      const options = createOptionStates(rawOptions)
      const parentId = option.id
      const separator = props.separator

      option.children.push(...options)

      let idCount = Math.max(...Array.from(optionIdMap.keys()).map(Number)) + 1

      options.forEach(option => {
        option.id = idCount++
        option.parent = parentId

        initOptionFull(option, separator)

        optionIdMap.set(option.id, option)
        optionValueMap.set(option.fullValue, option)
      })

      optionList.push(...options)
    }

    option.loaded = true
    option.loading = false

    const upstream = queryUpstreamOptions(option)
    upstream.forEach(option => {
      option.childrenLoaded = queryChildrenLoaded(option)
    })
  }

  if (depth < openedIds.value.length) {
    openedIds.value = openedIds.value.slice(0, depth)
  }

  openedIds.value.push(option.id)
  requestAnimationFrame(() => {
    getLast(panelElList.value)?.$el?.focus()
  })
}

function handleOptionSelect(option: CascaderOptionState, depth: number) {
  if (!option) return

  if (option.hasChild || option.children?.length) {
    handlePanelOpen(option, depth)
  } else {
    handleSingleSelect(option.fullValue)
  }
}

function queryUpstreamOptions(option: CascaderOptionState) {
  const options = [option]
  let parent = optionIdMap.get(option.parent)

  while (parent) {
    options.push(parent)
    parent = optionIdMap.get(parent.parent)
  }

  return options
}

function updateCheckedUpward(originalOption: CascaderOptionState) {
  let option = originalOption

  while (!isNull(option.parent)) {
    const parent = optionIdMap.get(option.parent)

    if (!parent) break

    if (option.checked === parent.checked && option.partial === parent.partial) {
      break
    }

    if (option.checked) {
      parent.checked = parent.children.every(item => item.disabled || item.checked)
      parent.partial = !parent.checked
    } else {
      parent.checked = false
      parent.partial = parent.children.some(item => item.checked || item.partial)
    }

    option = parent
  }
}

function updateCheckedDown(originalOption: CascaderOptionState) {
  const checked = originalOption.checked
  const partial = originalOption.partial

  const loop = [...originalOption.children]

  let option

  while (loop.length) {
    option = loop.shift()!

    if (option.disabled) continue

    option.checked = checked
    option.partial = partial

    if (option.children?.length) {
      loop.push(...option.children)
    }
  }
}

function handleOptionCheck(option: CascaderOptionState) {
  if (!option) return

  const options = Array.from(optionIdMap.values())
  const checked = !option.checked

  if (!props.multiple) {
    for (let i = 0, len = options.length; i < len; ++i) {
      options[i].checked = false
    }

    option.checked = checked
    option.partial = false

    return handleSingleSelect(option.fullValue)
  }

  option.checked = checked
  option.partial = false

  if (!props.noCascaded) {
    const originalOptions = [option].concat(
      options.filter(option => option.disabled && option.checked),
    )

    for (let i = 0, len = originalOptions.length; i < len; ++i) {
      const option = originalOptions[i]

      updateCheckedUpward(option)
      updateCheckedDown(option)
    }
  }

  emitEvent(props[checked ? 'onSelect' : 'onCancel'], option.fullValue, option.data)
  emitMultipleChange()
}

function emitMultipleChange() {
  const options = Array.from(optionIdMap.values())
  const selectedOptions = props.noCascaded
    ? options.filter(option => option.checked)
    : options.filter(option => option.checked && !(option.hasChild || option.children?.length))

  const selectedValues: string[] = []
  const selectedLabels: string[] = []

  const values: (string | number)[][] = []
  const dataList: Data[][] = []
  const briefLabel = props.briefLabel

  selectedOptions.forEach(option => {
    selectedValues.push(option.fullValue)
    selectedLabels.push(briefLabel ? option.label : option.fullLabel)

    const { value, data } = queryArrayMeta(option.fullValue)

    values.push(value)
    dataList.push(data)
  })

  currentValues.value = selectedValues
  currentLabels.value = selectedLabels

  if (usingMerged.value) {
    if (isAsyncLoad.value) {
      mergedValues.value = options.filter(option => option.checked).map(option => option.fullValue)
    }

    updateMergedProps()
  }

  if (usingMerged.value && isAsyncLoad.value) {
    values.length = 0
    dataList.length = 0

    mergedValues.value.forEach(fullValue => {
      const option = optionValueMap.get(fullValue)

      if (option) {
        const { value, data } = queryArrayMeta(option.fullValue)

        values.push(value)
        dataList.push(data)
      }
    })
  }

  emitChangeEvent(values, dataList)
  nextTick(updatePopper)
}

function handleSingleSelect(fullValue: string) {
  const option = optionValueMap.get(fullValue)

  if (!option) return

  emitEvent(props.onSelect, fullValue, option.data)

  if (fullValue) {
    currentValues.value[0] = fullValue
    currentLabels.value[0] = props.briefLabel ? option.label : option.fullLabel
  } else {
    currentValues.value.length = 0
    currentLabels.value.length = 0
  }

  const { value, data } = queryArrayMeta(fullValue)

  emitChangeEvent(value, data)
  setVisible(false)
}

function emitChangeEvent(value: CascaderValue, data: Data[] | Data[][]) {
  emittedValue.value = value

  nextTick(() => {
    outsideChanged = false

    emit('update:value', value)
    setFieldValue(value)
    emitEvent(props.onChange as ChangeListener, value, data)
    validateField()
  })
}

function queryArrayMeta(fullValue: string) {
  let option = optionValueMap.get(fullValue)!

  if (!option) return { value: [], data: [] }

  const value = [option.value]
  const data = [option.data]

  while (option.parent) {
    const parent = optionIdMap.get(option.parent)

    if (!parent) break

    value.push(parent.value)
    data.push(parent.data)
    option = parent
  }

  return {
    value: value.reverse(),
    data: data.reverse(),
  }
}

function toggleVisible(visible = !currentVisible.value) {
  if (props.disabled || readonly.value) return

  setVisible(visible)
}

function handleClickOutside() {
  restTipShow.value = false
  emitEvent(props.onClickOutside)

  if (props.outsideClose && currentVisible.value) {
    setVisible(false)
    outsideClosed = true
    emitEvent(props.onOutsideClose)
  }
}

function handleClear() {
  if (props.disabled || readonly.value) return

  if (props.clearable) {
    const prev = emittedValue.value

    currentValues.value.length = 0
    currentLabels.value.length = 0
    mergedValues.value.length = 0
    mergedLabels.value.length = 0
    openedIds.value.length = 0
    emittedValue.value = prev?.length === 0 ? prev : []
    restTipShow.value = false

    for (const option of optionIdMap.values()) {
      option.checked = false
      option.partial = false
    }

    if (prev?.length !== 0) {
      emit('update:value', emittedValue.value)
      emitEvent(props.onChange as ChangeListener, emittedValue.value, [])
    }

    emitEvent(props.onClear)
    clearField(emittedValue.value)
  }
}

function toggleShowRestTip() {
  if (!currentVisible.value) {
    restTipShow.value = !restTipShow.value
  } else {
    restTipShow.value = false
  }
}

function handleTipClose(fullValue: string) {
  if (props.disabled || readonly.value) return

  if (props.multiple) {
    handleOptionCheck(optionValueMap.get(fullValue)!)
  } else {
    handleSingleSelect(fullValue)
  }
}

function handlePanelKeyOpen(option: CascaderOptionState, depth: number) {
  handlePanelOpen(option, depth)

  requestAnimationFrame(() => {
    const panel = getLast(panelElList.value)

    if (panel && panel.currentHitting < 0) {
      panel.currentHitting = panel.options.findIndex(option => option.id === prevClosedId)

      if (panel.currentHitting < 0) {
        panel.currentHitting = 0
      }
    }
  })
}

function handlePanelBack() {
  prevClosedId = openedIds.value.pop()!
}

function handlePanelsEnter() {
  requestAnimationFrame(() => {
    isPopperShow.value = true
  })
}
</script>

<template>
  <div
    :id="idFor"
    ref="wrapper"
    :class="className"
    role="group"
    :aria-disabled="toAttrValue(props.disabled)"
    :aria-expanded="toAttrValue(currentVisible)"
    aria-haspopup="dialog"
    :aria-labelledby="labelId"
    @click="toggleVisible()"
  >
    <div
      ref="reference"
      :class="selectorClass"
      tabindex="0"
      @keydown.space.prevent="toggleVisible()"
      @keydown.tab="toggleVisible(false)"
    >
      <div
        v-if="hasPrefix"
        :class="[nh.be('icon'), nh.be('prefix')]"
        :style="{ color: props.prefixColor }"
      >
        <slot name="prefix">
          <Renderer :renderer="props.slots.prefix">
            <Icon :icon="props.prefix"></Icon>
          </Renderer>
        </slot>
      </div>
      <div :class="nh.be('control')">
        <slot name="control">
          <Renderer :renderer="props.slots.control">
            <Overflow
              v-if="props.multiple"
              inherit
              :class="[nh.be('tags')]"
              :items="templateValues"
              :max-count="props.maxTagCount"
              @rest-change="restTagCount = $event"
            >
              <template #default="{ item, index }">
                <Tag
                  inherit
                  :class="nh.be('tag')"
                  :type="props.tagType"
                  closable
                  :disabled="props.disabled"
                  @click.stop="toggleVisible()"
                  @close="handleTipClose(item)"
                >
                  {{ templateLabels[index] }}
                </Tag>
              </template>
              <template #counter="{ count }">
                <Tag
                  v-if="props.noRestTip"
                  inherit
                  :class="[nh.be('tag'), nh.be('counter')]"
                  :type="props.tagType"
                  :disabled="props.disabled"
                >
                  {{ `+${count}` }}
                </Tag>
                <span v-else>
                  <Tooltip
                    inherit
                    :transfer="false"
                    :visible="restTipShow"
                    trigger="custom"
                    placement="top-end"
                    :tip-class="nh.be('rest-tip')"
                    @click.stop="toggleShowRestTip"
                  >
                    <template #trigger>
                      <Tag
                        inherit
                        :class="[nh.be('tag'), nh.be('counter')]"
                        :type="props.tagType"
                        :disabled="props.disabled"
                      >
                        {{ `+${count}` }}
                      </Tag>
                    </template>
                    <NativeScroll inherit use-y-bar>
                      <template v-for="(item, index) in templateValues" :key="index">
                        <Tag
                          v-if="index >= templateValues.length - restTagCount"
                          inherit
                          :class="nh.be('tag')"
                          closable
                          :type="props.tagType"
                          :disabled="props.disabled"
                          @close="handleTipClose(item)"
                        >
                          {{ templateLabels[index] }}
                        </Tag>
                      </template>
                    </NativeScroll>
                  </Tooltip>
                </span>
              </template>
            </Overflow>
            <template v-else>
              {{ currentLabels[0] }}
            </template>
            <span
              v-if="(props.placeholder ?? locale.placeholder) && !hasValue"
              :class="nh.be('placeholder')"
            >
              {{ props.placeholder ?? locale.placeholder }}
            </span>
          </Renderer>
        </slot>
      </div>
      <div
        v-if="!props.noSuffix"
        :class="[nh.be('icon'), nh.be('suffix')]"
        :style="{
          color: props.suffixColor,
          opacity: showClear || props.loading ? '0%' : ''
        }"
      >
        <slot name="suffix">
          <Renderer :renderer="props.slots.suffix">
            <Icon
              v-if="props.suffix"
              :icon="props.suffix"
              :class="{
                [nh.be('arrow')]: !props.staticSuffix
              }"
            ></Icon>
            <Icon v-else v-bind="icons.angleDown" :class="nh.be('arrow')"></Icon>
          </Renderer>
        </slot>
      </div>
      <div
        v-else-if="props.clearable || props.loading"
        :class="[nh.be('icon'), nh.bem('icon', 'placeholder'), nh.be('suffix')]"
      ></div>
      <Transition :name="nh.ns('fade')" appear>
        <button
          v-if="showClear"
          :class="[nh.be('icon'), nh.be('clear')]"
          type="button"
          tabindex="-1"
          :aria-label="locale.ariaLabel.clear"
          @click.stop="handleClear"
        >
          <Icon v-bind="icons.clear"></Icon>
        </button>
        <div v-else-if="props.loading" :class="[nh.be('icon'), nh.be('loading')]">
          <Icon
            v-bind="icons.loading"
            :effect="props.loadingEffect || icons.loading.effect"
            :icon="props.loadingIcon || icons.loading.icon"
          ></Icon>
        </div>
      </Transition>
    </div>
    <Popper
      ref="popper"
      :class="[nh.be('popper'), nh.ns('input-vars'), nh.bs('vars')]"
      :visible="currentVisible"
      :to="transferTo"
      :transition="props.transitionName"
      :alive="props.popperAlive ?? !transferTo"
      @click.stop
      @enter="handlePanelsEnter"
    >
      <div
        :class="{
          [nh.be('panels')]: true,
          [nh.bem('panels', 'empty')]: !optionsList[0] || !optionsList[0].length
        }"
      >
        <template v-if="optionsList[0] && optionsList[0].length">
          <CascaderPanel
            v-for="(items, index) in optionsList"
            :key="index"
            :ref="(panel: any) => panel && panelElList.push(panel)"
            :options="items"
            :opened-id="openedIds[index]"
            :values="currentValues"
            :ready="isPopperShow"
            :multiple="props.multiple"
            :is-async="isAsyncLoad"
            :merged="usingMerged"
            :no-cascaded="props.noCascaded"
            :labeled-by="labelId"
            @select="handleOptionSelect($event, index)"
            @hover="usingHover && handlePanelOpen($event, index)"
            @check="handleOptionCheck($event)"
            @open="handlePanelKeyOpen($event, index)"
            @back="handlePanelBack"
            @close="currentVisible = false"
          >
            <template v-if="slots.default || props.slots.default" #default="payload">
              <slot v-bind="payload">
                <Renderer :renderer="props.slots.default" :data="payload"></Renderer>
              </slot>
            </template>
            <template v-if="slots.label || props.slots.label" #label="payload">
              <slot name="label" v-bind="payload">
                <Renderer :renderer="props.slots.label" :data="payload"></Renderer>
              </slot>
            </template>
          </CascaderPanel>
        </template>
        <div v-else :class="nh.be('empty')" :style="{ width: `${selectorWidth}px` }">
          <slot name="empty">
            <Renderer :renderer="props.slots.empty">
              {{ props.emptyText ?? locale.empty }}
            </Renderer>
          </slot>
        </div>
      </div>
    </Popper>
  </div>
</template>
